Repository navigation
release v1.41.0 DEV to main promotion - #2911
Merged
Merged
Conversation
The per-path-item temporarily-unavailable flag (admin-toggled, already honored on the mission page) was dropped by the App Space data path, so a disabled item still rendered as an active, clickable kiosk card. - path-item-task-resolver: shapeItem/buildPathItems now carry temporarilyUnavailable + unavailableReason on direct items (group-expanded children stamped false, matching the mission page). - app-space-gating: isTutorialOpenable returns false for unavailable items — the single chokepoint that both blocks the click and strips .clickable. - AppSpace.vue: 'Temporarily Unavailable' badge + reason, greyed/delinked card. Tests: buildPathItems passthrough, gating block, and a mounted-component test proving badge render + no window.open on click.
…sable feat(#2864): honor temporarily-unavailable path items in App Space
…usters) Models parallelism within a track entirely on the existing schema: shared itemOrder = parallel; 2+ GROUP rows at one Order = side-by-side branch lanes; N TUTORIAL rows at one Order = free-order cluster. Per-lane reward via GROUP+prize on the same CompletionPathItems row (resolver appends a synthetic CHECKPOINT at read time); join reward via a downstream CHECKPOINT. No schema change.
…ze in app-space payload
Add test/e2e/app-space-parallel.spec.ts: post-deploy smoke spec that loads /app-space/ and, when a fork track is seeded on the env, asserts two .lane columns in the fork segment, a .lane__name label on each, and at least one .step--locked downstream item (the join checkpoint locked for anonymous visitors). Self-skips without SMOKE_BASE_URL (repo e2e convention, #1338). Admin editor verification (Part A, read-only): - itemOrder has no @assert.unique constraint on CompletionPathItems (db/schema.cds) and is an editable integer column with no uniqueness validator in the Fiori Elements table — duplicate Order values for parallel rows are freely settable. - prize_ID has no conditional @UI.Hidden tied to taskType; the Prize column is visible for every row type including GROUP items — group + prize authoring on one row is fully supported. No admin code change required.
…llel-design # Conflicts: # hugo-apps/src/app-space/AppSpace.vue
…l-design App Space: parallel paths within a track (fork lanes + free-order clusters) (#2887)
…hildren #2888's buildPathSegments hardcoded temporarilyUnavailable:false on GROUP-expanded children in both the lone-GROUP linear branch and the fork-lane branch (finding M2 / documented limitation). A temporarily- unavailable GROUP branch therefore stayed navigable and, if its children were previously COMPLETED, its lane could spuriously earn the prize + downstream unlock. Propagate the parent GROUP path-item's temporarilyUnavailable and unavailableReason onto each expanded child via shapeItem's existing `unavail` arg (both the lone-GROUP linear path and the fork-lane GROUP children). This fixes both consequences with no client-logic change and no schema change: - render: AppSpace.vue laneUnavailable + the linear timeline already read the per-item flag, so the branch now dims; - completion-counting: isComplete()/itemDone() return false for an unavailable item, so isLaneComplete/isLaneTutorialsComplete no longer mark an unavailable lane complete. getEventProgress / buildPathItems (linear tracks) still pass null for GROUP children, so that path keeps its prior non-inherited behavior. The synthetic lane-prize CHECKPOINT stays available=false: an unavailable lane never completes, so its prize cannot unlock anyway. Updated the now-stale shapeItem and AppSpace.vue laneUnavailable comments that described the old non-propagation behavior. Tests: new buildPathSegments unit cases (lone-GROUP linear, fork-lane, available-control) in test/unit/app-space-segments.test.js.
…ilable-propagation fix(#2890): propagate GROUP temporarilyUnavailable to expanded lane children
App Space `isTrackComplete` marked a segmented track complete when ANY checkpoint unlocked (`checkpoints.some(isSegmentItemUnlocked)`). For a hypothetical track with a non-terminal plain CHECKPOINT (authored at a lower order than later tutorials), that early checkpoint unlocking would flag the whole track complete while downstream work remained. Make the rule prize-aware instead of a naive highest-order restriction: - lane-prize checkpoints (prizeId set) are authored to earn the reward the moment their single lane finishes → ANY unlocked one completes the track (keeps `some` semantics); - plain downstream checkpoints (no prizeId) are joins → only the TERMINAL (highest-order) one completes the track. Add client unit tests for isTrackComplete/progressPercent over fork payloads (the aggregate rule was previously only exercised indirectly via the barcode tests): the non-terminal-checkpoint bug guard fails against the old `some()` rule, and a lane-prize-only payload guards against an over-eager highest-order-only regression. Follow-up to #2887 (PR #2888), finding M1.
…st-checkpoint fix(#2889): key isTrackComplete off the highest-order checkpoint
A PROD deploy on 2026-10-21 shipped the approuter built from an uncommitted working-tree edit to hugo/assets/css/sap-fundamental.css, so Hugo fingerprinted the dirty CSS and the approuter served a hash the content pipeline (built from committed origin/main) never bakes -> 404 on every tutorial on /tutorials/ and /tutorials-qa/ (PROD-down). guardPrimaryTreeBranch (Step 0) missed it: a dirty tree keeps the same HEAD SHA, so both the PROD (branch) and DEV/QA (HEAD===origin SHA) paths pass with a dirty hugo/assets/** file. Add deploy-mta.cjs Step 0.5 (guardCleanBuildInputs, all envs, before build:all): git status --porcelain -- hugo/assets hugo/layouts, die(1) listing any tracked-modified build input. Pure parser dirtyBuildInputs exported + unit-tested; ignores untracked so build:all-generated hugo/data/*.json never false-positives (scope excludes hugo/data). Read-only -> runs under --dry-run and --skip-build. Doc the regression in tutorials-ims-gotchas.md + CLAUDE.md pointer.
Code-review minor: the gate only blocks TRACKED changes, so plain stash of the file suffices; -u also stashes untracked (broader than needed and at odds with the repo's multi-worktree bare-stash warning).
…gate fix(#2886): refuse deploy when Hugo fingerprint inputs are dirty
…et-gate fix(#2885): gate content publish on approuter-served CSS/JS assets (QA + CLI)
The admin PAT scope (headless /admin-pat/* + /graphql-pat, #2574) is validated and enforced end-to-end in the backend, but the mint dialog only offered read/write — so a user following the docs could never mint an admin-scoped PAT and GET /admin-pat/* 403'd with 'lacking required roles: [Admin]'. - srv/pat-service.{cds,js}: add unbound PatService.patCapabilities() returning {adminScopeAvailable} = PAT_ADMIN_SCOPE_ENABLED AND req.user.is('Admin'), mirroring the handleMintPAT gate. - MintDialog.fragment.xml + PatActionsController.js: the admin scope item (and its hint) are hidden by default and shown only when patCapabilities reports the caller may mint one; fails closed on error. - docs: clarify the admin-scope gate in api-consumption.md; add a regression note to tutorials-ims-gotchas.md (don't make the admin item statically visible — reintroduces the non-admin 403). - test/pat-admin-grant.test.js: cover patCapabilities (flag+admin -> true, non-admin -> false, flag off -> false). Backend gates, AdminGrants, feature flag, approuter routes and TTL clamp are unchanged.
…-pat-scope-ui fix(#2857): surface admin PAT scope in mint UI (gated to admins)
…of hard-fail An admin-write save to a Tutorials row whose slug was retired from the source repos dispatches a slug-targeted rebuild for that slug. fetch-tutorials then hard-fails (process.exit 1) because discovery no longer returns it, aborting the whole run and firing a false #1373 channel alert. Distinguish auto-dispatched runs (admin-write/scheduled/admin-action) from human manual dispatches via the trigger-source input: on an all-unknown (fatal) filter, auto-dispatched runs exit 0 (nothing to publish) while manual runs keep the #433 author-typo fail-fast. Wire trigger-source into the fetch step env; extract isAutoDispatchedTrigger() with unit tests.
… built) Records the parallel-any 'any one -> one shared prize' exploration and why it was deferred: themed fork lanes with per-lane prizes (shipped #2887/#2889) plus the server-side max-2 prize cap cover the requirement, so no shared-prize code change is needed. Kept for a future event that genuinely needs any-one-of-N -> one shared prize.
) The fork drop and join up-stub connectors were positioned at each lane COLUMN center ((li+0.5)/N*100%), but a lane's bubble sits a fixed ~1.8rem from its column's left edge (1px border + 0.6rem padding + 1.125rem half-bubble), not at the column center. The gap scaled with column width, so on 2-lane forks the vertical connectors landed ~200px right of the bubbles they should touch (measured in a browser repro). Fix: position drops/ups at calc((100% - (N-1)rem)/N*li + li*rem + 1.8rem) via a laneBubbleLeft() helper; span both bars from lane-0 to lane-(N-1); drop the fork-connector's left margin so its origin matches .lanes; anchor the fork stem + join down-stub to the backbone bubble center (1.175rem). Verified <=1px alignment across lane counts/widths in a static repro. Class names/structure unchanged — AppSpace.segments.test.ts still passes.
…el-any-prize fix(app-space): align fork/join connectors to lane bubble centers (#2896)
…noop fix(rebuild): no-op auto-dispatched rebuild for retired slug instead of hard-fail
…n fingerprint
Root cause of the recurring post-deploy "approuter does NOT serve referenced
CSS" guard failures (and the stale/unstyled tutorial pages before the guard
existed): `sap-fundamental.css` (generated by `build:css`, postcss) and
`fundamental-styles-icon.css` (generated by `build:icon-subset`) were BUILD
OUTPUTS committed to git. The committed bytes had drifted from what the build
now emits:
committed sap-fundamental: 7f01bb57… fresh build:css: 9dbb07c7…
committed icon: b34fb316… fresh build:icon: 317e9916…
A deploy runs `build:all` → regenerates both → fingerprint A. rebuild-content.yml
drives `/tmp/hugo` directly (NOT `build:all`) → used the STALE committed copy →
fingerprint B. A ≠ B → the approuter-asset guard correctly blocked publish.
Both generators are deterministic (verified: two runs → identical bytes), so the
only variable was the stale committed artifact.
Fix — make both publish paths generate from source, never from git:
* .gitignore + `git rm --cached` both generated CSS files.
* rebuild-content.yml: add a "Build fingerprinted CSS" step
(build:icon-subset + build:css) BEFORE the Hugo build, mirroring the
existing "explicit step rather than build:all" fetch steps. npm ci already
installs fundamental-styles, which build:icon-subset needs.
* Deploy path unchanged — build:all already runs both before mbt build.
Interaction with #2886: deploy-mta.cjs guardCleanBuildInputs (dirty-tree skew
guard) is generic and skips untracked/ignored paths, so it no longer watches
these outputs — and no longer needs to. Its real job (protect committed SOURCES
in hugo/assets + hugo/layouts) is intact; this change removes the committed
generated output entirely, so it CANNOT be dirty — a stronger guarantee than the
guard alone.
NOTE: rebuild-content.yml's auto-trigger runs off `main`, so this workflow
change only takes effect for auto-rebuilds once promoted DEV→main.
…s-drift fix(build): stop committing generated CSS so deploy and rebuild agree on fingerprint
…low-up) PR #2899 aligned the fork drops/ups to each lane's bubble center, but left the horizontal bar starting at lane-0's bubble (~61px) while the vertical stem sits at the backbone bubble center (1.175rem ≈ 51px) — a ~10px horizontal jog where the stem meets the bar (visible at the top of the fork in the CAP track). Measured on the live DEV page (event 10000015, CAP track): stem left 50.8, bar left 60.8 → 10px gap. Setting the bar's left to 1.175rem (the stem x) closes it: re-measured gap_stem_to_bar = 0. Verified against the real rendered DOM, not a static repro. Same change applied to the join-connector bar for tracks that have a downstream join.
…r-gap fix(app-space): connect fork/join bar to the backbone stem (#2896 follow-up)
…, claimed names, event stats Probed DEV HANA (read-only): all 14,686 CLAIMED PrizeRecords carry a real prize name (0 null); statuses are CLAIMED/EARNED only; 16-row Prizes catalog. Folds builder-check resolutions in as calls. Awaiting approval before build.
… null-count guard (#badge-v2)
…Contestant action (#badge-v2) - getContestant completion counts now scoped to the scanned event (match the event-stats panel; falls back to all-events when eventId absent/unresolvable) - claimPrizeForContestant invoked via POST with body params (it is an OData action, not a function) — fixes the pre-existing GET-vs-action mismatch
fix(scanner): badge-scanner v2 — camera, real prize count, identity, claimed prizes, event stats (#badge-v2)
A `<MyApp />` inside a backtick code span was being double-escaped to the literal text `<MyApp />` on the rendered page. Root cause: sanitizeLine (stripDangerousHtml) runs on raw markdown line-by-line and pseudo-tag-escapes any unknown tag-like token to `<...>` — including tokens inside inline code spans. Goldmark then renders the backtick span as <code> and HTML-escapes the `&` a second time, so `<` becomes `<` becomes the visible literal `<`. Fix: stash inline code spans (`...`, ``...``) before pseudo-tag/sanitize processing and restore them verbatim at the end, mirroring the existing autolink-stash pattern. Stashing happens before autolink extraction so a `<url>` inside a span stays literal. Pseudo-tags OUTSIDE code spans are still escaped as before. - 3 new unit tests (inline span, mixed line, double-backtick span) - catalog smoke classifier taught the same code-span exception - verified 0 regressions across 1498 corpus tutorials Fixes the JSX tip rendering reported on the React tutorials.
…udotag-escape fix(parser): preserve pseudo-tags inside inline code spans
…outer Root cause (verified live via Playwright on DEV): - All contestant> bindings were relative (claimedCount) not absolute (/claimedCount). With no binding context on the contestant JSONModel, they resolved to null -> prizeCount rendered 'null of 2' while the model held 0; identity/stats blank; visible-state expressions wrong. Injecting the absolute path on the live control: 'null of 2' -> '0 of 2' -> '1 of 2'. - manifest declared the App view as BOTH rootView AND routing target 'main' (same id) -> 'adding element with duplicate id container-scanner---App' throw on router init. App has a single screen; dropped the router and its Component.js getRouter().initialize() call. Untouched by #2902 (badge-scanner v2 only covered the Vue island + backend).
Root cause (confirmed live on DEV): useBarcodeScanner gated isSupported on
'BarcodeDetector' in window. That API is Chromium/Android-only — absent on
iOS Safari AND desktop Edge/Chrome in this deployment — so every such browser
fell through to showManualInput ('Camera scanning not available') despite
having a working camera (getUserMedia present, secure context).
- isSupported now = getUserMedia available in a secure context
(extracted to camera-support.ts, unit-tested).
- Scan loop uses native BarcodeDetector when present, else decodes
getUserMedia frames with jsQR (new dep, ~15KB) over a canvas.
- Manual-paste path retained as the fallback for no-camera/insecure cases.
Tests: hugo-apps/src/scanner-vue/__tests__/camera-support.test.ts (8 pass).
Follow-up to #2902 (badge-scanner v2), which shipped the BarcodeDetector-only gate.
…-apps fix(scanner): repair both scanner apps (null of 2 + camera not available)
The admin PAT scope was wired into app/admin/pats/ (the Fiori admin PATs list, #2857) — but users mint tokens from /me/tokens/, which is the Vue island hugo-apps/src/tokens/ApiTokens.vue, a DIFFERENT app. Its Access dropdown was hardcoded to read / readwrite with no admin option and no flag check, so flipping PAT_ADMIN_SCOPE_ENABLED had no visible effect on the page users actually use. - ApiTokens.vue: probe PatService.patCapabilities() on mount; show an 'admin' <option> only when adminScopeAvailable (flag ON AND live Admin role). onMint maps scope 'admin' → scopes:['admin']. Probe is shape-tolerant and fails CLOSED (option hidden on any error/non-JSON). - tests: admin option hidden when unavailable / on probe error; shown when available; mint sends scopes:['admin']. 12/12 island specs pass. Reuses the patCapabilities() backend from #2857 — no backend change.
…dmin-scope fix(#2574): add admin PAT scope to the /me/tokens/ mint UI (the app users actually use)
…name in UI5 Two independent badge-scanner bugs surfaced while testing the Vue port against DEV: 1. Vue claim POST got a bare HTTP 403. The approuter enforces CSRF on writes to /scanner/* (no csrfProtection:false on that route), and the Vue fetch sent no x-csrf-token. The UI5 app works only because its sap.ui.model.odata.v4.ODataModel does the fetch-token handshake for you. useScannerApi now fetches a token (GET x-csrf-token: fetch), sends it on the claim POST, and refetches+retries once on a 403 CSRF challenge (stale token). NOT an "already claimed" case — a duplicate claim returns HTTP 200 "cap reached (2 of 2)". 2. UI5 scanner showed the raw UID instead of the contestant name. The getContestant handler returns lastName/firstInitial (unit-tested), but the UI5 controller never read them and the view bound /uid as the title. Controller now reads both; view shows "LastName, F." with the UID underneath. The Vue app already rendered these correctly. Tests: scanner-vue claim suite 8/8 (incl. new CSRF handshake + retry cases); scanner-vue full suite 20/20; server getContestant unit 8/8.
…-and-contestant-fields fix(scanner): CSRF token on Vue claim POST + contestant name in UI5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 1.41.0 — DEV → main promotion
Minor release promoting the fixes and features merged to
DEVsince 1.40.4 tomainfor a PROD deploy.Version bumps
tutorials-ims).deploy/mta.yamltutorials-mcp)mta-mcp.yamlMinor bump: this window includes new features (App Space parallel paths, badge-scanner v2, temporarily-unavailable items in the kiosk).
Changes in this window (18 whats-new entries)
Features
Fixes
/me/tokens/mint UI users actually use.<MyApp />) inside inline code spans (React/JSX tutorials rendered escaped text).Maintenance
Deploy
Merging this PR does not redeploy — a separate MTA deploy is required (main MTA + the separate
tutorials-mcpMTA). No deploy authorization implied by this PR.🤖 Generated with Claude Code